//#!/usr/bin/env node
//
// WebSocket聊天服务器
//使用Node.js实现
//
//需要websocket模块。
//
//基于WebSocket和WebRTC的多用户聊天示例与双向视频
//打电话，包括在适当或必要时轮流使用。
//
//这个文件包含实现服务器端的JavaScript代码
//聊天系统的功能，包括用户ID管理，消息
//反射和路由私有消息，包括对
//通过未知的JSON对象发送，以支持自定义应用程序和信号
// WebRTC。
//
//需要Node.js和websocket模块(websocket - node):
//
//——http://nodejs.org/
//——https://github.com/theturtle32/WebSocket-Node
//
//阅读这个示例的工作原理:http://bit.ly/webrtc-from-chat
//
//任何版权都属于公共领域。
// http://creativecommons.org/publicdomain/zero/1.0/

"use strict";

var http = require('http');
var https = require('https');
var fs = require('fs');
var WebSocketServer = require('websocket').server;

// SSL密钥和要使用的证书文件的路径名
// HTTPS连接。

const keyFilePath = '/SSL.KEY'//"/etc/pki/tls/private/mdn-samples.mozilla.org.key";
const csrfilepath = './SSL.CSR'//"/etc/pki/tls/certs/mdn-samples.mozilla.org.crt";
  // CSR: fs.readFileSync('./SSL.CSR'),
  // KEY: fs.readFileSync('./SSL.KEY'),

//用于管理文本聊天用户列表。

var connectionArray = [];
var nextID = Date.now();
var appendToMakeUnique = 1;

//将日志信息输出到控制台

function log(text) {
  var time = new Date();

  console.log("[" + time.toLocaleTimeString() + "] " + text);
}

//如果您想实现对阻塞特定源的支持，可以这样做
//在哪里做。返回false来拒绝给定的WebSocket连接
//指定的原点。
function originIsAllowed(origin) {
  return true;    // We will accept all connections
}

//扫描用户列表，看看指定的名称是否惟一。如果是,
//返回true。否则,返回false。我们希望所有用户都是独一无二的
//名字。
function isUsernameUnique(name) {
  var isUnique = true;
  var i;

  for (i=0; i<connectionArray.length; i++) {
    if (connectionArray[i].username === name) {
      isUnique = false;
      break;
    }
  }
  return isUnique;
}

//发送一条消息(它已经是字符串化的JSON)给单个
// user，给定用户名。我们将其用于WebRTC信号，
//我们还可以用它来发私人短信。
function sendToOneUser(target, msgString) {
  var isUnique = true;
  var i;

  for (i=0; i<connectionArray.length; i++) {
    if (connectionArray[i].username === target) {
      connectionArray[i].sendUTF(msgString);
      break;
    }
  }
}

//扫描连接列表并返回指定的连接
// clientID。每个登录都有一个在会话期间不会改变的ID，
//因此可以跨用户名更改跟踪它。
function getConnectionForID(id) {
  var connect = null;
  var i;

  for (i=0; i<connectionArray.length; i++) {
    if (connectionArray[i].clientID === id) {
      connect = connectionArray[i];
      break;
    }
  }

  return connect;
}

//构建一个类型为“userlist”的消息对象，其中包含
//所有已连接的用户。用于增加新登录的用户，
//低效地处理名称更改通知。
function makeUserListMessage() {
  var userListMsg = {
    type: "userlist",
    users: []
  };
  var i;

 //将用户添加到列表中

  for (i=0; i<connectionArray.length; i++) {
    userListMsg.users.push(connectionArray[i].username);
  }

  return userListMsg;
}

//向所有聊天成员发送一条“userlist”消息。这是一种俗气的方式
//确保每个连接/删除都反映在每个地方。会更多
//高效地向每个用户发送简单的连接/删除消息，但这是
//对于这个简单的例子来说已经足够好了。
function sendUserListToAll() {
  var userListMsg = makeUserListMessage();
  var userListMsgStr = JSON.stringify(userListMsg);
  var i;

  for (i=0; i<connectionArray.length; i++) {
    connectionArray[i].sendUTF(userListMsgStr);
  }
}


//尝试加载SSL的密钥和证书文件，这样我们就可以
//执行HTTPS(非本地WebRTC需要)。

var httpsOptions = {
  key: null,
  csr: null
};

try {
  httpsOptions.key = fs.readFileSync(keyFilePath);
  try {
    httpsOptions.csr = fs.readFileSync(csrfilepath);
  } catch(err) {
    httpsOptions.key = null;
    httpsOptions.csr = null;
  }
} catch(err) {
  httpsOptions.key = null;
  httpsOptions.csr = null;
}

//尝试加载SSL的密钥和证书文件，这样我们就可以
//执行HTTPS(非本地WebRTC需要)。
var webServer = null;

try {
  if (httpsOptions.key && httpsOptions.csr) {
    webServer = https.createServer(httpsOptions, handleWebRequest);
  }
} catch(err) {
  webServer = null;
}

if (!webServer) {
  try {
    webServer = http.createServer({}, handleWebRequest);
  } catch(err) {
    webServer = null;
    log(`Error attempting to create HTTP(s) server: ${err.toString()}`);
  }
}


//我们的HTTPS服务器只提供WebSocket服务
//连接，所以每个请求只返回404。真正的网络
//请求由机器上的主服务器处理。如果你
//如果愿意，可以在这里返回真正的HTML并提供Web内容。

function handleWebRequest(request, response) {
  log ("Received request for " + request.url);
  // response.writeHead(200);
  response.writeHead(404);
  response.end();
}

//在分配给这个示例的端口上启动HTTPS服务器。
//这将很快转换为WebSocket端口。

webServer.listen(6503, function() {
  log("Server is listening on port 6503");
});

//通过将HTTPS服务器转换为一个WebSocket服务器来创建WebSocket服务器。

var wsServer = new WebSocketServer({
  httpServer: webServer,
  autoAcceptConnections: false
});

if (!wsServer) {
  log("ERROR: Unable to create WbeSocket server!");
}

//在我们的WebSocket服务器上设置一个“connect”消息处理程序。这是
//在用户连接到服务器端口时调用
// WebSocket协议。

wsServer.on('request', function(request) {
  if (!originIsAllowed(request.origin)) {
    request.reject();
    log("Connection from " + request.origin + " rejected.");
    return;
  }

//接受请求并连接。

  var connection = request.accept("json", request.origin);

 //将新连接添加到我们的连接列表中。

  log("Connection accepted from " + connection.remoteAddress + ".");
  connectionArray.push(connection);

  connection.clientID = nextID;
  nextID++;

//向新客户发送令牌;它返回一个“用户名”消息
//告诉我们他们想用什么用户名。

  var msg = {
    type: "id",
    id: connection.clientID
  };
  connection.sendUTF(JSON.stringify(msg));

//为通过WebSocket接收的“message”事件设置一个处理程序。这
//是客户端发送的消息，可以是与他人共享的文本
//用户，一个用户的私有消息(文本或信令)，或命令
//到服务器。
  connection.on('message', function(message) {
    if (message.type === 'utf8') {
      log("Received Message: " + message.utf8Data);

      //处理传入数据。

      var sendToClients = true;
      msg = JSON.parse(message.utf8Data);
      var connect = getConnectionForID(msg.id);

      //看一下传入的对象，并基于它采取行动
      //就其类型而言。传递未知的消息类型，
      //因为它们可以用来实现客户端特性。
      //带有“target”属性的消息只发送给用户
      //就叫这个名字。
      switch(msg.type) {
        //公共文本消息
        case "message":
          msg.name = connect.username;
          msg.text = msg.text.replace(/(<([^>]+)>)/ig, "");
          break;

        //用户名更改
        case "username":
          var nameChanged = false;
          var origName = msg.name;

          //在名称后面加上一个数字，确保名称是惟一的
          //如果不是;继续尝试，直到成功为止。
          while (!isUsernameUnique(msg.name)) {
            msg.name = origName + appendToMakeUnique;
            appendToMakeUnique++;
            nameChanged = true;
          }

          //如果必须更改名称，我们发送一个“rejectusername”
          //将消息返回给用户，让他们知道自己的名字
          //被服务器更改。
          if (nameChanged) {
            var changeMsg = {
              id: msg.id,
              type: "rejectusername",
              name: msg.name
            };
            connect.sendUTF(JSON.stringify(changeMsg));
          }

          //设置此连接的最终用户名并发送
          //向所有用户更新用户列表。是的，我们要寄满了
          //列表而不仅仅是更新。这是非常低效
          //但是这只是一个演示。不要在真正的应用程序中这样做。
          connect.username = msg.name;
          sendUserListToAll();
          sendToClients = false;  //我们已经发出了适当的答复
          break;
      }

    //将修改后的消息转换回JSON并发送出去
    //适用于指定的客户或所有客户。我们
    //传递没有特别处理的消息
    //在上面的select块中。这允许客户端这样做
    //交换信令和其他控制对象不受阻碍。

      if (sendToClients) {
        var msgString = JSON.stringify(msg);
        var i;

        //如果消息指定了目标用户名，则只发送目标用户名
        //给他们留言。否则，将其发送给每个用户。
        if (msg.target && msg.target !== undefined && msg.target.length !== 0) {
          sendToOneUser(msg.target, msgString);
        } else {
          for (i=0; i<connectionArray.length; i++) {
            connectionArray[i].sendUTF(msgString);
          }
        }
      }
    }
  });

  //处理WebSocket“关闭”事件;这意味着用户已经注销
  //或已断开连接。
  connection.on('close', function(reason, description) {
    //首先，从连接列表中删除连接。
    connectionArray = connectionArray.filter(function(el, idx, ar) {
      return el.connected;
    });

    //现在发送更新后的用户列表。再说一遍，请不要在a中这样做
    //真实的应用程序中。你的用户不会很喜欢你。
    sendUserListToAll();

    //构建和输出日志输出以获取关闭信息。

    var logMessage = "Connection closed: " + connection.remoteAddress + " (" +
                     reason;
    if (description !== null && description.length !== 0) {
      logMessage += ": " + description;
    }
    logMessage += ")";
    log(logMessage);
  });
});
log('试下')